/**
  * @(#)DatabaseManager.java
  *
  * Provide the abstract implementation of the database manager to provide the
  * interface for database initialization and function.
  *
  * @author Course Scheduler Team
  * 
  * @license GNU General Public License version 3 (GPLv3)
  *
  * This file is part of Course Scheduler, an open source, cross platform
  * course scheduling tool, configurable for most universities.
  *
  * Copyright (C) 2010-2012 Course Scheduler Team
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  * 
  */
package com.pollicitus.scheduler.database;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;

import com.pollicitus.scheduler.core.Application;
import com.pollicitus.scheduler.core.Manager;
import com.pollicitus.scheduler.core.NamespaceSettings;
import com.pollicitus.scheduler.core.Settings;
import com.pollicitus.scheduler.database.DatabaseClient;
import com.pollicitus.scheduler.database.DatabaseManager;

/**
 * Provide the abstract implementation of the database manager to provide the
 * interface for database initialization and function.
 * 
 * @author Course Scheduler Team
 *
 */
public abstract class DatabaseManager extends Manager {

	//public class constants
	public static final String CONTEXT_SEPARATOR = "#";
	
	//protected class constants
	protected static final String NAMESPACE = "database";
	protected static final int DATABASE_NAME_POSITION = 0;
	protected static final int DATABASE_CONTEXT_POSITION = 1;
		
	//private class variables
	private static DatabaseManager dbManager;
	private static Logger classLog;
	
	//protected instance variables
	protected Map<String, DatabaseClient> dbClients;
	
	/**
	 * Static initialization of the database manager client map. This
	 * map structure stores the thread clients which correspond to the
	 * specified database.
	 */
	{
		dbClients = new HashMap<String, DatabaseClient>();
	}
	
	/**
	 * Return the manager as configured in the properties files. This
	 * requires retrieval of the implementation specified in the primary
	 * config and loading the class definition. The implementation
	 * settings are loaded into the specified namespace, the instance is
	 * initialized and then returned. This method only performs logging
	 * for invocations that specify enableLogging = true.
	 * 
	 * @param settings the application settings instance
	 * @return the database manager responsible for this application invocation
	 * @throws FileNotFoundException if the implementation configuration file can not be found
	 * @throws IOException if there is a problem reading from the configuration file
	 * @throws InstantiationException if there is a problem instantiating the manager class
	 * @throws IllegalAccessException if there is a problem accessing the manager class
	 * @throws ClassNotFoundException if the class definition cannot be found for the manager
	 */
	public static DatabaseManager getInstance(Settings settings) throws FileNotFoundException, IOException, InstantiationException, IllegalAccessException, ClassNotFoundException{
		//properly wrap the primary settings manager
		NamespaceSettings dbProps = new NamespaceSettings(settings, NAMESPACE);
		
		//retrieve the logging settings for the database manager
		String loggingNamespace = dbProps.getProperty(LOGGING_NAMESPACE);
		classLog = Application.logManager.getLogger(loggingNamespace);
		classLog.info("Database module initializing");
		
		//retrieve the logging settings
		String managerClass = dbProps.getProperty(MANAGER_CLASS);
		String configFile = dbProps.getProperty(CONFIG_FILE);
		String settingsNamespace = dbProps.getProperty(MANAGER_NAMESPACE);
		classLog.debug("Detected the initialization class as {}", managerClass);
		classLog.debug("Detected the configuration file as {}", configFile);
		classLog.debug("Detected the settings namespace as {}", settingsNamespace);
		
		//retrieve an initialized instance of the database manager
		dbManager = Manager.getInstance(settings, managerClass, configFile, settingsNamespace);
		classLog.info("Successfully retrieved instance of the database manager {}", dbManager);
		
		return dbManager;
	}
	
	/**
	 * Retrieve an application database client for the specified database.
	 * This is the method that database manager implementations should implement
	 * in order to allow external components to access the database clients.
	 * This method is called by the getClient method in DatabaseManager which
	 * also performs database client memoization by database context.
	 * 
	 * @param databaseName the name of the database for which a client should be returned
	 * @param databaseContext the synchronization context for which a client should be returned
	 * @return the context specific database client for the specified database and context
	 */
	protected abstract DatabaseClient getDatabaseClient(String databaseName, String databaseContext);
	
	/**
	 * Return the default database context for the specified database. This is
	 * used as a fallback mechanism when an application component does not 
	 * specify a database synchronization context. Database manager implementations
	 * may specify a default context as desired, with the requirement that the
	 * same context must be returned for all calls to getDefaultClient(databaseName)
	 * during application execution (in order to insure thread-safety).
	 * 
	 * @param databaseName the name of the database for which the default context is needed
	 * @return the name of the context which should be used 
	 */
	protected abstract String getDefaultContext(String databaseName);
	
	/**
	 * Retrieve an application database client for the specified database.
	 * Each database client is specific to one application context for purposes
	 * of transaction sets, commits, and rollbacks. As such, the database manager
	 * implementations must return the same database client for subsequent calls
	 * to getDatabaseClient for the same database context.
	 * 
	 * @param databaseName the name of the database for which a client should be returned
	 * @param databaseContext the name of the context for which a client should be returned
	 * @return the database client for the database context
	 */
	public DatabaseClient getClient(String databaseName, String databaseContext){
		//check if a contextual database client already exists for the specified database
		log.trace("Checking for existing client for database {} using context", databaseName, databaseContext);		
		DatabaseClient client = dbClients.get(databaseName + CONTEXT_SEPARATOR + databaseContext);
		
		if(client == null){
			log.trace("Client does not exist for context, creating instance");
			client = getDatabaseClient(databaseName, databaseContext);
			client.setDatabaseName(databaseName);
			client.setDatabaseContext(databaseContext);
			dbClients.put(databaseName + CONTEXT_SEPARATOR + databaseContext, client);
		}
		
		log.debug("Returning database client for database {} context {}: {}",
				new Object[]{databaseName, databaseContext, client});
		//return the database client
		return client;
	}
	
	/**
	 * Retrieve an application database client for the specified database.
	 * Each database client is specific to one application context for purposes
	 * of transaction sets, commits, and rollbacks. As such, the database manager
	 * implementations must return the same database client for subsequent calls
	 * to getDatabaseClient for the same database context.
	 * 
	 * @param databaseContext the name of the database for which a client should be retrieved
	 * @return the database client for the database context
	 */
	public DatabaseClient getClient(String databaseContext){
		String[] dbc = null;
		
		//check if the specified database context includes both components		
		log.trace("Checking for database context in {}", databaseContext);
		if(!databaseContext.contains(CONTEXT_SEPARATOR)){
			log.trace("Database context missing. Only database name found in {}", databaseContext);
			String defaultContext = getDefaultContext(databaseContext); //safe because the database context only has the name portion
			log.trace("Found default database context: {}", defaultContext);
			dbc = new String[]{databaseContext, defaultContext};
		}{
			log.trace("Database context found in {} ", databaseContext);
			dbc = databaseContext.split(CONTEXT_SEPARATOR);
			log.trace("Using context {}", dbc[DATABASE_CONTEXT_POSITION]);
		}
		
		return getClient(dbc[DATABASE_NAME_POSITION],
						dbc[DATABASE_CONTEXT_POSITION]);
	}
	
	/**
	 * Close the specified database client by removing it from the 
	 * memoization structure and calling the Database Client's 
	 * internalClose method, which performs the actual cleanup
	 * of the database client's internal storage.
	 * 
	 * @param client the database client which should be closed
	 */
	public void closeClient(DatabaseClient client){
		log.trace("Preparing to close database client {}", client);
		
		//remove the client from memoization structure
		String database = client.getDatabaseName();
		String context = client.getDatabaseContext();
		String databaseContext = database + CONTEXT_SEPARATOR + context;
			
		try{
			//perform the internal closing of the database client
			log.debug("Closing database client {}", client);
			client.internalClose();
		} finally{
			log.trace("Removing the database client from the memoization structure for database context {}", databaseContext);
			dbClients.remove(databaseContext);
		}
	}
	
	/* (non-Javadoc)
	 * @see com.scheduler.core.Manager#finalize(com.scheduler.core.Settings)
	 * 
	 * Note that the DatabaseManager abstract class requires its implementation
	 * subclasses to call the closeClient(DatabaseClient) method for each 
	 * database client prior to closing the underlying manager connections
	 */
	@Override
	public abstract void finalize(Settings settings);
}
